// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Data;
using SnapObjects.Commons;
using JetBrains.Annotations;
using System.Collections.Generic;
using Oracle.ManagedDataAccess.Client;

namespace SnapObjects.Data.Oracle
{
    public class OracleTypeMapper : TypeMapper
    {
        private readonly DataContextOptions _options;

        //string
        private readonly OracleStringTypeMapping _fixedLengthUnicodeString
            = new OracleStringTypeMapping("nchar", dbType: DbType.StringFixedLength, unicode: true);

        private readonly OracleStringTypeMapping _variableLengthUnicodeString
            = new OracleStringTypeMapping("nvarchar", dbType: null, unicode: true);

        private readonly OracleStringTypeMapping _nvarchar2
            = new OracleStringTypeMapping("nvarchar2", dbType: null, unicode: true);

        private readonly OracleStringTypeMapping _char
            = new OracleStringTypeMapping("char", dbType: DbType.AnsiStringFixedLength);

        private readonly OracleStringTypeMapping _variableLengthAnsiString
            = new OracleStringTypeMapping("varchar", dbType: DbType.AnsiString);

        private readonly OracleClobTypeMapping _nclob
            = new OracleClobTypeMapping("nclob", OracleDbType.NClob, DbType.String, unicode: true);

        private readonly OracleClobTypeMapping _clob
            = new OracleClobTypeMapping("clob", OracleDbType.Clob, DbType.AnsiString, unicode: false);

        private readonly OracleClobTypeMapping _oracleLong
           = new OracleClobTypeMapping("long", OracleDbType.Long, DbType.AnsiString, unicode: false);

        private readonly InternalStringTypeMapping _internalStringTypeMapping;

        //binary
        private readonly OracleByteArrayTypeMapping _binary
            = new OracleByteArrayTypeMapping("binary", DbType.Binary);

        private readonly OracleByteArrayTypeMapping _binaryVarying
            = new OracleByteArrayTypeMapping("binary varying", DbType.Binary);

        private readonly OracleByteArrayTypeMapping _bfile
            = new OracleByteArrayTypeMapping("bfile", DbType.Binary);

        private readonly OracleByteTypeMapping _byte
            = new OracleByteTypeMapping("tinyint", DbType.Byte);

        private readonly OracleByteArrayTypeMapping _longRaw
            = new OracleByteArrayTypeMapping("Long Raw", DbType.Binary);

        private readonly OracleByteArrayTypeMapping _raw
            = new OracleByteArrayTypeMapping("Raw", DbType.Binary);

        private readonly OracleByteArrayTypeMapping _blob
            = new OracleByteArrayTypeMapping("blob", DbType.Binary);

        //number
        private readonly OracleIntTypeMapping _int = new OracleIntTypeMapping("int", DbType.Int32);

        private readonly OracleLongTypeMapping _long = new OracleLongTypeMapping("long", DbType.Int64);

        private readonly OracleShortTypeMapping _short = new OracleShortTypeMapping("short", DbType.Int16);

        private readonly OracleDoubleTypeMapping _binaryDouble
            = new OracleDoubleTypeMapping("binary_double", DbType.Double);

        private readonly OracleDoubleTypeMapping _double
            = new OracleDoubleTypeMapping("double", DbType.Double);

        private readonly OracleFloatTypeMapping _binaryFloat
            = new OracleFloatTypeMapping("binary_float", DbType.Single);

        private readonly OracleFloatTypeMapping _single
            = new OracleFloatTypeMapping("float");

        private readonly OracleDecimalTypeMapping _decimal
            = new OracleDecimalTypeMapping("decimal", DbType.Decimal);

        //date
        private readonly OracleDateTimeTypeMapping _date
            = new OracleDateTimeTypeMapping("date", dbType: DbType.Date);

        private readonly OracleDateTimeTypeMapping _timestamp
            = new OracleDateTimeTypeMapping("datetime", dbType: DbType.DateTime);

        private readonly OracleTimeSpanTypeMapping _time
            = new OracleTimeSpanTypeMapping("time");

        private readonly OracleDateTimeOffsetTypeMapping _datetimeoffset
            = new OracleDateTimeOffsetTypeMapping("datetimeoffset", DbType.DateTimeOffset);

        //cursor
        private readonly OracleCursorTypeMapping _cursor = new OracleCursorTypeMapping("sys_refcursor");

        private readonly OracleCursorTypeMapping _refCursor = new OracleCursorTypeMapping("ref cursor");

        private readonly OracleBoolTypeMapping _bool = new OracleBoolTypeMapping("bit", DbType.Boolean);

        private readonly OracleTimeSpanTypeMapping _intervalDay
            = new OracleTimeSpanTypeMapping("interval day");

        private readonly OracleLongTypeMapping _intervalYear
            = new OracleLongTypeMapping("interval year");

        private readonly Dictionary<string, TypeMapping> _dbTypeMappings;
        private readonly Dictionary<Type, TypeMapping> _clrTypeMappings;

        public OracleTypeMapper() : this(null)
        {
        }

        public OracleTypeMapper(IDataContextOptions contextOptions) : base(contextOptions)
        {
            _options = contextOptions is DataContextOptions opt ? opt : null;

            _internalStringTypeMapping = new InternalStringTypeMapping(_options);

            _dbTypeMappings = new Dictionary<string, TypeMapping>(StringComparer.OrdinalIgnoreCase)
            {
                //string
                { "varchar", _variableLengthAnsiString },
                { "varchar2", _variableLengthAnsiString} ,
                { "nvarchar2", _nvarchar2},
                { "xmltype", _variableLengthAnsiString},
                { "rowid", _variableLengthAnsiString},
                { "urowid", _variableLengthUnicodeString},
                { "char varying", _variableLengthAnsiString },
                { "char", _char },
                { "character varying", _variableLengthAnsiString },
                { "character", _char },
                { "long", _oracleLong},
                { "national char varying", _variableLengthUnicodeString },
                { "national character varying", _variableLengthUnicodeString },
                { "national character", _fixedLengthUnicodeString },
                { "nchar", _fixedLengthUnicodeString },
                { "nvarchar", _variableLengthUnicodeString },
                { "clob", _clob} ,
                { "nclob", _nclob},
                //binary
                { "bfile", _bfile},
                { "blob", _blob},
                { "binary varying", _binaryVarying },
                { "binary", _binary },
                { "binary_double", _binaryDouble},
                { "binary_float", _binaryFloat} ,
                { "raw", _raw},
                { "longraw", _longRaw} ,
                //boolean
                { "bit", _bool},
                { "boolean", _bool},
                { "pl/sql boolean", _bool },
                //number
                { "dec", _decimal },
                { "decimal", _decimal },
                { "double", _double} ,
                { "float", _decimal },
                { "int", _int },
                { "integer", _int },
                { "real", _decimal },
                { "smallint", _short },
                { "single", _single} ,
                { "short", _short} ,
                { "number", _decimal },
                //date
                { "date", _date },
                { "timestamp", _timestamp} ,
                { "timestampltz", _timestamp} ,
                { "timestamptz", _timestamp} ,
                { "timestamp with time zone", _timestamp},
                { "timestamp with local time zone", _timestamp},
                //cursor
                { "sys_refcursor", _cursor},
                { "ref cursor",  _refCursor},
                //other
                { "interval day", _intervalDay},
                { "interval day to second", _intervalDay},
                { "interval year", _intervalYear} ,
                { "interval year to month", _intervalYear} ,
            };

            // Note: sbyte, ushort, uint, char and ulong type mappings are not supported by SQL Server.
            // We would need the type conversions feature to allow this to work - see https://github.com/aspnet/EntityFramework/issues/242.
            _clrTypeMappings = new Dictionary<Type, TypeMapping>
            {
                { typeof(int), _int },
                { typeof(long), _long },
                { typeof(DateTime), _timestamp },
                { typeof(bool), _bool },
                { typeof(byte), _byte },
                { typeof(double), _double },
                { typeof(DateTimeOffset), _datetimeoffset },
                { typeof(short), _short },
                { typeof(float), _single },
                { typeof(decimal), _decimal },
                { typeof(TimeSpan), _time },
                { typeof(char), _char },
                { typeof(ICursor), _cursor}
            };
        }

        protected override IReadOnlyDictionary<Type, TypeMapping> GetClrTypeMappings()
        {
            return _clrTypeMappings;
        }

        protected override IReadOnlyDictionary<string, TypeMapping> GetDbTypeMappings()
        {
            return _dbTypeMappings;
        }

        public override TypeMapping FindMapping(Type clrType)
        {
            Check.NotNull(clrType, nameof(clrType));

            if (clrType.IsArray && clrType != typeof(byte[]))
            {
                clrType = clrType.GetElementType();
            }

            clrType = TypeHelper.UnwrapEnumType(TypeHelper.UnwrapNullableType(clrType));

            return clrType == typeof(string)
                ? _internalStringTypeMapping
                : (clrType == typeof(byte[])
                    ? _blob
                    : base.FindMapping(clrType));
        }

        protected override TypeMapping CreateMappingFromDbType([NotNull] string dbTypeName)
        {
            if (dbTypeName.Contains("number"))
            {
                return this.GetNumberTypeMapping(dbTypeName);
            }

            return base.CreateMappingFromDbType(dbTypeName);
        }

        private TypeMapping GetNumberTypeMapping(string dbTypeName)
        {
            var array = dbTypeName.Split(',');
            var newTypeName = "decimal";

            if (array.Length == 2)
            {
                var precisionParsed = Int32.TryParse(array[0].Substring(array[0].IndexOf("(") + 1), out var dataPrecision);
                var scaleParsed = Int32.TryParse(array[1].Trim(')'), out var dataScale);

                if (scaleParsed && dataScale > 0)
                {
                    if (precisionParsed)
                    {
                        if (dataPrecision <= 7)
                        {
                            newTypeName = "single";
                        }
                        else if (dataPrecision <= 15)
                        {
                            newTypeName = "double";
                        }
                    }
                }
                else if (precisionParsed)
                {
                    if (dataPrecision <= 4)
                    {
                        newTypeName = "short";
                    }
                    else if (dataPrecision <= 9)
                    {
                        newTypeName = "int";
                    }
                    else if (dataPrecision > 9 && dataPrecision <= 18)
                    {
                        return _long;
                    }
                }
            }

            if (this.GetDbTypeMappings().TryGetValue(newTypeName, out var mapping))
            {
                if (mapping.DbTypeName.Equals(newTypeName, StringComparison.OrdinalIgnoreCase))
                {
                    return mapping;
                }
            }

            return null;
        }
    }
}
